![]() |
|
|||||
Ausnahme-Behandler (exception handler) Die Ausnahme ist immer eine Instanz einer Ausnahme-Klasse, die den entsprechenden Laufzeitfehler identifiziert und wird von der JVM an einen passenden Ausnahme-Behandler weitergereicht (siehe 7.2.2). throw: Explizit wird eine Ausnahme durch throw throwableExpression; ausgelöst, wobei throwableExpression eine Instanz vom (Sub-)Typ Throwable ergeben muss. BeispieleIn der folgenden Methode wird eine Ausnahme durch die JVM ausgelöst, wenn der Divisor Null ist: Implizites Auslösen einer Ausnahme static int intDiv (int i, int j) { return i/j; // bei einer Ausnahme erfolgt kein return } Hat j den Wert 0, erzeugt die JVM eine Instanz der Klasse ArithmeticException und aktiviert die Ausnahme. In der nächsten Methode wird bei einem null-Argument eine Instanz von NullPointerException erschaffen und mittels throw ausgelöst: Explizites Auslösen einer Ausnahme class Point { int x,y; /*...*/ } static double distanceFromOrigin (Point p) { if (p==null) throw new java.lang.NullPointerException("p ist null"); return Math.sqrt(p.x*p.x+p.y*p.y); } Bei diesen beiden Methoden wird die return-Anweisung beim Auftreten einer Ausnahme nicht mehr ausgeführt. 7.2.2 Ausnahme-Behandlung (exception handling)
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 1. | Wird eine Ausnahme ausgelöst, gilt sie als unbehandelt und führt zum sofortigen Abbruch des normalen Programmablaufs. |
| 2. | Die JVM sucht als Nächstes im umgebenden Code bzw. in den aufrufenden Methoden so lange nach einer try-Anweisung, bis sie einen passenden catch-Block findet oder aber die oberste Methode der Threads, main() oder run(), verlassen muss. |
Unbehandelte Ausnahme: Abbruch!
| 3. | Das Verlassen des obersten Methoden-Blocks führt bei einer unbehandelten Ausnahme zum Abbruch (der Thread). |
| 4. | Nur eine try-Anweisung kann Ausnahmen behandeln: |
try { tryStatementSequenz } [ catch (ExceptionType1 e1) { exceptionHandlerSequenz1 } ... catch (ExceptionTypeN eN) { exceptionHandlerSequenzN } ] [ finally { finallyStatementSequenz } ]Dem try-Block muss zumindest ein optionaler catch- oder finally-Block folgen.
catch-Behandlung von Ausnahmen
| 5. | Sofern catch-Blöcke vorhanden sind, |
| werden nacheinander die einzigen Parameter ExceptionTypeX darauf untersucht, ob die unbehandelte Ausnahme-Instanz ein (Sub-) Typ ist. |
| wird bei einem passenden ExceptionTypeX der zugehörige catch-Block ausgeführt, alle nachfolgenden catch-Blöcke übersprungen und die Ausnahme gilt als behandelt. |
| kann hierdurch erneut eine Ausnahme ausgelöst werden. |
finally ist
unabhängig von Ausnahmen
| 6. | Sofern ein finally-Block vorhanden ist, |
| wird er immer am Ende der try-Anweisung ausgeführt, egal, ob eine Ausnahme aufgetreten, behandelt oder nicht behandelt ist. |
| kann eine unbehandelte Ausnahme durch eine return-Anweisung »verloren” gehen oder aber durch die Auslösung einer neuen Ausnahme ersetzt werden. |
Ausnahme
unbehandelt? Propagieren!
| 7. | Wurde die Ausnahme behandelt und keine neue ausgelöst, wird die Ausführung nach der try-Anweisung fortgesetzt. Ansonsten wiederholt sich die Suche, beginnend mit dem zweiten Schritt. |
Ausnahmen können also nur mit Hilfe von catch-Ausnahme-Parametern erkannt und behandelt werden.
Der finally-Block kann nur indirekt Ausnahmen durch Überschreiben oder Missachtung entfernen, was durchaus zu Problemen führen kann (siehe hierzu 7.3.3).
Aktivitäts-Diagramme:
Darstellung von Prozessen in UML
Aktivitäts-Diagramme wurden in Kapitel 4, Modellierung und UML, zwar nicht formal vorgestellt, sind aber zur Darstellung von Prozessabläufen und Bearbeitungsschritten recht reizvoll.
Start und Ende des Ablaufs sind unschwer an den massiven Kreisen zu erkennen. Die Bearbeitung (Aktivität) wird in Rechtecken dargestellt und folgt den Pfeilen und Entscheidungsrauten, wobei die jeweilige Bedingung (guard) in eckigen Klammern erfüllt sein muss.
Objekte, die beteiligt sind, und Kommentare runden das Diagramm ab.
Aktivitäts-Diagramm
zur Behandlung von Ausnahmen
|
In Abb. 7.1 wird der Versuch unternommen, das generelle Muster der Ausnahmebehandlung – analog zu den genannten Bearbeitungsschritten – anhand eines Aktivitäts-Diagramms darzustellen.
Auslösen einer NullPointerException
Zuerst wird in der Methode distanceFromOrigin() (siehe 7.2.1) eine NullPointerException ausgelöst:
double d= 0.0; try { d= distanceFromOrigin(null); System.out.println(d); // wird nicht ausgeführt ¨ } catch (Exception e) { d= Double.NaN; } finally { System.out.println(d); } // :: NaN
Erklärung: Die Ausnahme wird in distanceFromOrigin() ausgelöst und nicht behandelt. Also sucht die JVM im umgebenden Block.
Die Suche endet in der try-Anweisung, die die Methode enthält.
Diese wird sofort abgebrochen, ohne println() in Zeile ¨ auszuführen.
Da NullPointerException eine Subklasse von Exception ist, ist der einzige catch-Block der passende Ausnahme-Behandler und die Ausnahme gilt als behandelt.
Abschließend wird noch der finally-Block ausgeführt.
Variationen:
catch-Blöcke, finally-Block
Die folgende Klasse Test ist aufgrund ihrer verschiedenen Versionen interessant:
public class Test { public static void main(String[] args) { int[] iarr= {0,1,2}; int i=0; /* Version 1 */ try { while(true) System.out.println(1/iarr[i++]); } catch (IndexOutOfBoundsException e) { System.out.println("Ind"); } /* Version 2 catch (ArithmeticException e) { System.out.println("Ari"); } */ /* Version 3 finally { System.out.println("fin"); } */ } }
Version 1 (nur erster catch-Block): In der while-Schleife tritt sofort eine ArithmeticException auf. Da ArithmeticException keine Subklasse von IndexOutOfBoundsException ist, passt der erste catch-Block nicht. Die JVM muss die Applikation Test abbrechen.
Version 2 (inkl. zweitem catch-Block): Nun gibt es einen passenden catch-Block. Die Ausnahme gilt als behandelt und das Programm beendet normal (:: Ari).
Version 3 (nur erster catch-Block und finally): Es gibt zwar keinen passenden catch-Block, allerdings wird vor dem Programmabbruch erst finally ausgeführt (:: fin).
Es gibt noch einige interessante Details, die in den Bearbeitungsschritten (siehe 7.2.2) noch nicht angesprochen wurden.
Folgen einem try-Block mehrere catch-Blöcke, muss aufgrund der sequenziellen Abarbeitung der catch-Blöcke folgende Reihenfolge eingehalten werden:
Reihenfolge der
catch-Exceptions
| catch-Blöcke mit spezielleren Ausnahmen müssen vor catch-Blöcken mit generellen Ausnahmen angegeben werden. |
Der Compiler prüft die Einhaltung der Regel und erzeugt ggf. eine »unreachable«-Fehlermeldung:
Nach dem fünften Schritt in 7.2.2 verdeckt ein generellerer Ausnahmetyp alle speziellen, d.h. alle nachfolgenden Subtypen, die somit nicht ausgeführt werden könnten.Das kleine Beispiel »Test« provoziert eine Fehlermeldung:
public class Test { public static void main(String[] args) {
Compiler erzeugt »unreachable« -Meldung
try { System.out.println(1/0); } catch (Exception e) { } // C-Fehler catch (ArithmeticException e) { } } }
Ein Vertauschen der beiden catch-Blöcke löst das Problem.
Ausnahme-Behandlungen können ineinander geschachtelt werden, was sehr angenehm sein kann.
try-block try-block try-block ... catch-blocks finally-block catch-blocks finally-block
Ausführung von geschachtelten try-Anweisungen
Gemäß dem fünften Schritt in 7.2.2 werden bei Auslösen einer inneren Ausnahme zuerst alle zugehörigen inneren catch-Blöcke durchsucht. Sollte es keinen passenden catch-Block geben, erfolgt nach dem zweiten Schritt sukzessive die Suche nach einer try-Anweisung in den umgebenden Blöcken.
static int div (Integer i1, Integer i2) { try { try { return i1.intValue()/i2.intValue(); } catch (NullPointerException npe) { return 0; } } catch (ArithmeticException ae) { return Integer.MAX_VALUE; } /* finally { System.out.print("fin "); } */ }
Integer i= new Integer(7); System.out.println(div(i,null)); // :: 0
System.out.println(div(i,new Integer(0))); // :: 2147483647
System.out.println(div(i,new Integer(2))); // :: 3
Version mit finally-Block : Vor jeder Zahl erfolgt die Ausgabe fin.
Funktion von finally in der
try-Anweisung 
In einem finally-Block hat man keinen direkten Zugriff auf die ausgelöste Ausnahme. Allerdings kann man – meist aus Versehen – die Ausnahme durchaus entfernen oder mit einer anderen überschreiben. Wichtig ist die folgende Regel:
| Es kann nur jeweils eine unbehandelte Ausnahme von einer Methode bzw. einem Block nach außen übergeben werden. |
Aufgrund dieser Regel wird eine Ausnahme, die im try-Block nicht behandelt wird, durch eine Ausnahme überschrieben, die im finally-Block ausgelöst und nicht behandelt wird.
finally-Problem:
verlorene Ausnahme!
Die Methode finallyTest1() erzeugt eine verlorene Ausnahme:
static void finallyTest1() throws Exception { try { throw new Exception("try"); } finally { throw new Exception("fin"); } }
Der Aufruf erzeugt nach der o.a. Regel:
try { finallyTest1(); } catch (Exception e) { System.out.println(e.toString()); // :: java.lang.Exception: fin
}
Die Variation finallyTest2() beseitigt das Problem der verlorenen Ausnahme:
finally-Variation mit innerem
try-catch
static void finallyTest2() throws Exception { try { throw new Exception("try"); } finally { try { throw new Exception("fin"); } catch (Exception e) { /* wird hier behandelt */ } } }
In der folgenden Methode neverThrowsExceptions() hat die return-Anweisung in finally einen recht subtilen Nebeneffekt, der im zweiten Punkt des sechsten Schritts in 7.2.2 angesprochen wurde.

Wirkung von return im finally-Block
Es ist das Problem der verlorenen unbehandelten Ausnahme:
static void neverThrowsExceptions() throws Exception { try { throw new Exception(); } // geht verloren! finally { return; // Methode wird normal verlassen! } // die Exception wurde implizit behandelt! }
| Eine Methode, deren finally-Block eine return-Anweisung ausführt, liefert keine Ausnahmen nach außen. |
Auch in catch-Blöcken können wieder neue Ausnahmen ausgelöst werden.
Rethrowing: Erneutes Auslösen derselben Ausnahme
| Unter Rethrowing versteht man die erneute Aktivierung derselben Ausnahme mittels throw, die dem catch-Block übergeben wurde. |
Dies ist in Fällen sinnvoll, in denen catch – eventuell abhängig von bestimmten Bedingungen – die Ausnahme nur teilweise oder gar nicht behandeln kann und die Ausnahme deshalb weiterreichen muss.
In der Methode mean() wird eine Division durch 0 abgefangen (d.h., das Array hat keine Elemente). Wird mean() dagegen mit null aufgerufen, wird die Ausnahme nach außen weitergereicht.
Rethrowing einer RuntimeException
public static int mean(int[] iarr) { try { int m= 0; for (int i=0; i<iarr.length; m+=iarr[i++]); return m/iarr.length; } catch (RuntimeException e) { if (e instanceof NullPointerException) throw e; else return 0; } }
Hier drei verschiedene Aufrufe:
System.out.println(mean(new int[] {1,2,3})); // :: 2
System.out.println(mean(new int[] {})); // :: 0
System.out.println(mean(null)); // NullPointerException
Das Basis-Package java.lang stellt bereits eine umfangreiche Klassen-Hierarchie von Ausnahmen bereit, die Auswirkungen auf den Compiler und die Art der Verwendung hat.
Throwable stellt die Basisklasse aller Ausnahmen dar (Abb. 7.2). Nach Java-Konvention sollen
| von der Klasse Throwable – obwohl nicht abstrakt – keine eigenen Instanzen erzeugt werden und |
| möglichst nur Ausnahmen von Leaf-Klassen3 erzeugt werden. |
Begründet wird der letzte Punkt damit, dass die Hierarchie eine disjunkte Klassifikation der Laufzeitfehler darstellt, die den Fehler immer weiter präzisiert. Eine Leaf-Klasse bzw. -Instanz ist also informativer.
Ausnahme-Hierarchie: eine disjunkte Ausnahme-Kassifikation
|
Nach Throwable folgen zwei große Kategorien von Fehlern:
| Exception ist die Basisklasse für alle Ausnahmen, die im Code abgefangen und behandelt werden sollen. |
| Error ist die Basisklasse für alle Fehler, die als nicht behebbar angesehen werden, also auch im Code nicht abgefangen werden sollen. |
Mit anderen Worten: Error-Ausnahmen sollen tabu sein.
checked vs. unchecked
Exceptions
Die Begriffe checked bzw. unchecked Exceptions stehen für Ausnahmen, die im Code geprüft oder deklariert bzw. nicht geprüft werden müssen.
Für Ausnahme-Klassen, die zu den checked Exceptions zählen, gilt folgende Regel:
Der Compiler stellt sicher, dass checked Exceptions
Compiler-Prüfung von checked Exceptions
| nur dann in Methoden nicht behandelt werden brauchen, wenn sie im Methoden-Kopf mittels throws deklariert sind: |
[mModifiers] ResultType methodName (parameterList) throws Throwable1[,...ThrowableN]
Warnung zu
unchecked Exceptions
| in einer übergeordneten Methode entweder mittels try-catch abgefangen oder erneut im Kopf mittels throws deklariert werden. |
Für unchecked Exceptions gibt es keine Regel, nur einen Warnhinweis:
| Diese Art von Ausnahmen kann zu jedem Zeitpunkt von der JVM oder explizit durch Code ausgelöst werden und führt dann ohne Behandlung immer zum Abbruch der Ausführung (Thread). |
Error/RuntimeException sind unchecked
|
Zu den unchecked Exceptions gehören alle Error- und RuntimeException-Klassen bzw. -Subklassen, zu den checked Exceptions dann die restlichen (siehe Abb. 7.2). |
Der Vorteil des Konzepts von checked Exceptions liegt in ihrer expliziten Natur. Man erkennt sie sofort bei Verwendung und hat nur die Alternative zwischen Behandeln und erneutem Weiterreichen.
checked bzw. unchecked
Exception: Compiler vs. JVM
Die Unterscheidung der Ausnahme-Klassen in checked bzw. unchecked ist »magic«, d.h. kann nicht durch ein Marker-Interface erfolgen.4 Eo ipso wird – man ahnt es bereits – die Einhaltung der Checked-Exception-Regel nur vom Compiler geprüft.
| Die JVM kennt keine Unterschiede zwischen checked und unchecked Exceptions. |
Throwable:
nur logisch abstrakt
Ein Blick in die Implementation von Throwable zeigt – im Gegensatz zu dem suggestiven Interface-Namen – eine voll implementierte Klasse, die allen Subklassen wie Exception oder Error praktisch bereits die Arbeit abnimmt.
Die wohl wichtigste Eigenschaft von Throwable, und damit aller Subklassen; ist die Fähigkeit, bei der Instanzierung einer Ausnahme eine Fehlerbeschreibung anzugeben.
Java bietet dazu – sofern erwünscht – einen ausführlichen Execution Stack Trace, d.h. eine Lokalisierung des Ursprungs und seiner Proliferation durch den ausgeführten Code.5
Da Throwable nicht selbst instanziert werden soll, erübrigt sich die Darstellung von Konstruktoren bzw. Methoden.
Error – »tödlicher« Laufzeitfehler
Wie bereits in 7.4.1 erwähnt, signalisieren Fehler vom Typ Error: »Hände weg vom Programm«. Interessant ist nur, welche Fehler die Java-Entwickler darunter verstehen.
Die größte Gruppe, Ausnahmen vom Typ LinkageError, können zwar nicht im Programm-Code behandelt werden, müssen aber darauf untersucht werden, warum eine Klasse zur Laufzeit nicht eingebunden werden kann. Die häufigste Ausnahme NoClassDefFoundError kennt dagegen jeder Java-Programmierer.6
Ausnahmen vom Typ VirtualMachineError drücken Fehler innerhalb der JVM aus, wobei OutOfMemoryError und StackOverflowError »von außen« beeinflusst werden können.
Die Klasse ThreadDeath gehört zwar logisch nicht zum Typ Error, wurde hier aber platziert, um nicht mit einem catch(Exception e) versehentlich abgefangen zu werden.
Diese Art der Ausnahme wird durch den mutwilligen Thread-Tod mittels stop() ausgelöst und erzeugt – im Gegensatz zu anderen – keine Meldung. Man kann ihn also schlichtweg ignorieren.
Java-Konvention zum Typ Exception
Alle für den Code interessanten Ausnahmen sind vom Typ Exception, da sie entweder im Programm behandelt werden müssen oder fehlerhaften Code signalisieren, der verbessert werden muss.
| enden alle Namen von Ausnahmen dieses Typs mit Exception. |
| werden eigene Ausnahme-Klassen als Subklassen von Exception deklariert, und zwar normalerweise als checked Exception. |
RuntimeException
signalisiert einen Programmierfehler
Ausnahmen vom Typ RuntimeException signalisieren, einfach gesagt, Fehler im Code, die der Compiler leider nicht aufdecken konnte.
| Robuster Code muss so geschrieben werden, das Ausnahmen vom Typ RuntimeException nicht mehr auftreten. |
So einfach diese Aussage, so schwierig ihre Umsetzung. Denn diese Fehler
| müssen nicht explizit abgefangen werden. |
| werden häufig durch unzulässige Argumente beim Methodenaufruf ausgelöst. |
Der zweite Punkt fällt dann unter das Thema Kontraktmanagement.7
Gerade deshalb werden in der Tabelle 6.1 zu den häufigsten RuntimeExceptions exemplarisch einfache Vermeidungsstrategien angegeben.
Vermeidung von RuntimeExceptions
| RuntimeException | Wird verhindert durch: |
| ArithmeticException | Vor div- bzw. mod-Operationen int-Operanden vorher prüfen |
| IndexOutOfBoundsException | String- oder Array-Index prüfen |
| ClassCastException | Mit instanceof-Operator den Typ prüfen |
| NullPointerException | Referenz auf nicht null prüfen |
| NumberFormatException | Konvertierungen von Strings nach numerische Typen in try-catch einbetten |
Aufgrund des Lazy-Programmer-Syndroms fallen die meisten Ausnahmen in den Packages der Java-Plattform und anderer Anbieter unter die Kategorie »checked Exceptions«.
Checked Exceptions: unvermeidliche Umgebungsfehler
Im Gegensatz zu RuntimeException enthält diese Kategorie Fehler, die durch die Programmumgebung und nicht durch eigenen fehlerhaften Code verursacht werden.
Hierunter fallen u.a. Klassen, die nicht (mehr) gefunden werden, Ein- und Ausgabefehler, Kommunikations-, Netzwerk- oder andere Hardwareprobleme.
Insbesondere diese Fehler müssen in einem robusten Programm unbedingt abgefangen werden, und es müssen – je nach Aufgabe – Strategien implementiert werden, wie diesen Fehlern zu begegnen ist.8
Beim Design von Methoden in einer Basisklasse bzw. einem Interface muss berücksichtigt werden, ob ein Überschreiben dieser Methode eventuell eine checked Exceptions notwendig macht. Denn es gilt die Regel:
Overriding bzw. Überschreiben von Ausnahmen
| Overriding einer Methode, die eine checked Exception deklariert, kann nur durch eine Methode erfolgen, die alternativ |
| Ausnahmen vom selben Typ oder Subtyp oder |
| keine Ausnahmen |
deklariert.9
Erklärung: Diese »einengende« Vererbungsrestriktion musste für Klassen und Interfaces eingeführt werden. Der Grund liegt in der Kombination aus Substitutions- und Checked-Exception-Regel:
Denn ohne die Overriding-Regel könnten Instanzen einer Subklasse eine Ausnahme auslösen, die in der Superklasse nicht deklariert ist.
Eingesetzt in Code, der nur Ausnahmen der Superklasse abfangen muss, würden sie dann locker die Regel für checked Exceptions (siehe 7.4.2) aushebeln.
Overriding von Ausnahmen bei Vererbung
interface I { void f() /* throws Exception */ ; } class A { void f() throws Exception{} }
// C-Fehler, da f() in I keine Exception deklariert class B extends A implements I { public void f() throws Exception {} }
Wird im Interface I die Methode f() analog zu A deklariert, gibt es aufgrund der Regel keinen Compiler-Fehler in Klasse B.
In den nächsten zwei Beispielen wird die Ausnahme-Hierarchie des Packages java.io verwendet (siehe Abb. 7.3).
|
interface I { void f() throws FileNotFoundException; } class A { void f() throws EOFException{} }
class B extends A implements I {
// die nachfolgenden Deklarationen sind nicht möglich public void f() throws FileNotFoundException{} // C-Fehler public void f() throws EOFException{} // C-Fehler public void f() throws FileNotFoundException, EOFException {} // C-Fehler // einer der nachfolgenden Deklarationen ist möglich public void f() {} // ok! public void f() throws Error {} // auch ok! }
Auch bei Hierarchien gilt die Overriding-Regel für Ausnahmen, wie das nächste Beispiel zeigt:
interface I1 { void f() throws IOException; }
interface I2 extends I1 { void f() throws ObjectStreamException; }
class B implements I2 { // nur noch (Sub-)Typen von ObjectStreamException möglich public void f() throws InvalidClassException {} }
Die Klasse B muss sich an Interface I2 orientieren.
Abschließend die Konsequenz, formuliert als Design-Regel:
Notwendige Ausnahmen in Basisklassen
|
Die Standard-Plattform enthält eine unüberschaubare Zahl von speziellen Ausnahme-Klassen, auf die man – sofern sie passen – zurückgreifen kann.
Natürlich kann man in einem eigenen Package – analog zu java.io – auch eigene Ausnahme-Klassen deklarieren. Ob man eine checked oder unchecked Exception deklariert, soll erst im folgenden Abschnitt behandelt werden. Zuerst interessiert der Mechanismus.

Muster für die Deklaration neuer Ausnahmen
class NewException extends ExistingException {
public NewException () { // evtl. Aufruf von super(...); }
public NewException (String description) { super(description); // explizite Fehler-Beschreibung } }
Ist ExistingException eine unchecked Exception, ist dies NewException ebenfalls.
Mit super(description) wird eine Fehlerbeschreibung bis nach Throwable durchgereicht, der einen entsprechenden Konstruktor bereitstellt:
public class Throwable implements java.io.Serializable { public Throwable(String message) { fillInStackTrace(); detailMessage = message; } //... }
Aufgrund des Marker-Interfaces Serializable können alle Ausnahmen gespeichert werden.10
Eine gültige Teile-Nummer besteht aus maximal zehn Stellen und enthält nur Ziffern oder Bindestriche.11 Es soll eine PrimaryKeyException im Konstruktor ausgelöst werden, wenn die Teile-Nummer ungültig ist. Diese soll explizit abgefangen werden.
class PrimaryKeyException extends Exception { // checked!
public PrimaryKeyException () { super("Fehlerhafter Teile-Primärschlüssel!"); } public PrimaryKeyException (String description) { super(description); } }
class Teil { String nr; Teil(String nr) throws PrimaryKeyException { if (nr.length()>10) throw new PrimaryKeyException(); try { Long.parseLong(nr.replace('-','0')); } catch (NumberFormatException e) { // Umwandlung einer throw new PrimaryKeyException(); // Runtime-Exception } this.nr=nr; } //... }
Bisher wurden Ausnahme-Mechanismen anhand bestehender Hierarchien vorgestellt und gezeigt, wie man korrekt reagiert.
Aber die eigentlich schwierig Frage, wie Ausnahmen in eigenen Applikationen sinnvoll und effizient eingesetzt werden, ist noch unbeantwortet.
Soviel vorab: Es gibt keine Ausnahme-Pattern, jedoch durchaus Idiome. Die Ausnahme-Behandlung ist mit anderen Worten an die jeweilige Sprache gebunden, und Antworten auf Fragen zum optimalen Ausnahme-Design bewegen sich gerade in Java auf dünnem Eis.12
Ausnahmen als
Ersatz für andere Kontrollstrukturen?
| Ausnahmen dienen nicht als Ersatz für andere Kontrollstrukturen. |
Das folgende Code-Segment ist ein extremes Beispiel:
double[] val= {1.,2.,3.}; double sum=0.; try { int i=0; while (true) sum+= val[i++]; } catch(Exception e) {} // ignoriert Index-Überschreitung! System.out.println(sum); // :: 6.0
Natürlich sind die Dinge nicht immer so klar wie in diesem Beispiel.
Unter Kontrakten – auch Assertions genannt – versteht man formale Eigenschaften, die vom Client einer Methode eingehalten werden müssen oder auch von der Methode selbst (siehe auch 6.12.1).
Eine häufige Kontraktverletzung besteht darin, die Preconditions (Vorbedingungen) beim Aufruf einer Methode nicht einzuhalten. Deshalb:
Ausnahmen: Einsatz bei
Kontrakt- Missachtung
| Robuste Methoden erzwingen die Einhaltung ihrer Preconditions mit Hilfe von Ausnahmen. |
Diese Regel ist nicht ganz so trivial in der Umsetzung, denn es gibt Alternativen.
Default-Werte
bei Kontraktverletzung
Default-Alternative: Werden Vorbedingungen nicht eingehalten, kann man mit Hilfe von Default-Werten die Operationen durchführen. Dies muss für den Client allerdings klar erkennbar sein, da er ansonsten von den Ergebnissen sehr überrascht sein könnte.
Ausnahme-Auswahl: Zur Wahl stehen checked vs. unchecked Exceptions. Beim Typ RuntimeException ist dem Client freigestellt, ob er mittels try-catch fehlerhafte Argument abfängt oder ob er darauf verzichten will. Im anderen Fall lässt man ihm keine Wahl (siehe 7.7.3).
Fehlerhafte Argumente bei Konstruktoren durch Default-Werte zu ersetzen, ist nicht gerade genial. Wozu ist wohl ein No-Arg-Konstruktor da?
Ohne die Hilfe von Ausnahmen kann sich ein Konstruktor nicht wirksam gegen fehlerhafte Aufrufe wehren, da konventionelle Fehlermitteilungen durch Rückgabewerte entfallen.

Ausnahme verhindert Instanz-Anlage
| Bei Auslösen einer Ausnahme in einem Konstruktor wird die Instanz nicht angelegt, und eine Referenz auf die Instanz hat weiterhin den alten Wert.13 14 |
class Teil { static final String mes= "Fehlerhafter Teile-Schlüssel!"; String nr;
// throws nicht notwendig, aber eine explizite Drohung! Teil(String nr) throws IllegalArgumentException { if (nr.length()>10) throw new IllegalArgumentException(mes); try { Long.parseLong(nr.replace('-','0')); } catch (NumberFormatException e) { throw new IllegalArgumentException(mes); } this.nr=nr; } }
Der Vorteil einer Unchecked-Variante liegt beim Client, der nun die Wahl hat:
public class Test { public static void main(String[] args) { String nr="234-x"; Teil t= null;
try { t= new Teil(nr); } catch (RuntimeException re) { System.out.println(t==null); // :: true } // ohne try Programm-Abbruch mit Ausnahme new Teil(nr); } }
Die JVM unterscheidet überhaupt keine Ausnahmen, der Java-Compiler kennt nur checked und unchecked Exceptions, eine hilfreiche, aber viel zu grobe Unterteilung.
Ausnahmen
differenziert nach Kommunikationsart
|
Die Kommunikation zwischen Sender (Server) und Empfänger (Client) mittels Ausnahmen sollte man ein wenig genauer differenzieren (siehe Abb. 7.4).
Ausnahmen, die zu einer Unterbrechung des normalen Programmablaufs führen, können auch Kommunikations-Signale sein:
| Begreift man eine Ausnahme als besonderes Signal15 des Senders, dann ist es ein Interrupt-Objekt, welches den normalen Programmablauf eines eventuell unbekannten Empfängers unterbricht. |
Spätestens bei den Threads wird klar, das Ausnahmen als Signale auch zu Kommunikationsaufgaben herangezogen werden, was aber leider nur sehr ungenügend von der Java-Sprache unterstützt wird.
Ausnahme:
Signalisieren
von Code- oder Umgebungsfehler
Fehler-Signale können – der Interpretation der Java-Entwickler folgend – in Fehler im Code und Umgebungsfehler unterschieden werden
Fehler im Code müssen letztendlich entfernt werden (siehe 7.4.6).
Hat man für diese Art von Laufzeitfehlern checked Exceptions gewählt, steht man anschließend vor vielen try-catch-Anweisungen, die den Code sinnlos aufblähen. Dies führt zum ersten Separations-Idiom.
RuntimeException: geeignet für Fehler im Code
| Exception-Separation: Unterscheide Fehler im Code von Umgebungsfehlern und signalisiere Codierungsfehler mit Hilfe einer Subklasse von RuntimeException. |
Die Nichteinhaltung von Kontrakten fällt häufiger unter die Rubrik »Fehler im Code«. Ein unzulässiges null-Argument kann durchaus mit einer NullPointerException geahndet werden. Ein Client hat ja die Wahl, den Kontrakt einzuhalten.
RuntimeException: ungeeignet für Umgebungsfehler
Umgebungsfehler dürfen nicht ignoriert werden. Hierfür ist eine anonyme RuntimeException nicht sinnvoll, da man bei der Benutzung des Services die try-catch-Anweisungen nicht weglassen darf.
Normale Reaktion: Die Server-Methode erklärt die Ausnahme im Kopf der Methode mittels throws als Subtyp von Exception (nicht Runtime!) und erwartet, dass sich der Client um das Problem kümmert.
Error: tötlicher Umgebungsfehler
Harte Reaktion: Die Server-Methode löst eine Ausnahme vom Typ Error aus. Als Resultat stirbt wahrscheinlich auch der Client, da dieser mit catch(Exception e) {...} keinen Error abfangen kann.
Fatale Fehler:
der Client
entscheidet
Der Client unterscheidet die bei ihm eintreffenden Ausnahmen in fatale Fehler, die ihn seinerseits zum Abbruch zwingen, und in nicht fatale.
| Was fatal ist, bestimmt der Client. |
Nicht fatale Fehler:
Wiederholen/Ignorieren
| Nicht fatale Fehler können mit der Strategie »Wiederholen« (mit/ohne Variation) oder »Ignorieren« behandelt werden. |
Ein gescheiterter Verbindungsaufbau oder Sperrversuch eines Datensatzes führt zwar zu einer Ausnahme, aber eventuell erst nach mehrmaliger Wiederholung zu einem fatalen Fehler.
In einer Multimedia-Anwendung mag es Laufzeitfehler geben, die aber bis zu einem Grenzwert ignoriert werden müssen.16 Dies führt zum zweiten Separations-Idiom.